package site.immonkey.ftptest;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import site.immonkey.ftptest.beans.FtpFile;
import site.immonkey.ftptest.beans.FtpResponse;
import site.immonkey.ftptest.beans.LogListener;
import site.immonkey.ftptest.beans.StreamHandler;
import site.immonkey.ftptest.utils.IOUtils;

public class FtpUtil {
	public final static int DEFAULT_PORT = 21;

	protected String host;
	protected int port = -1;
	protected boolean showHideFile = true;
	protected String encode = "UTF-8";

	protected Socket connection;
	protected BufferedReader _reader;
	protected BufferedWriter _writer;
	protected LogListener logListener; // = LogListener.DEFAULT_LISTENER;

	public FtpUtil(String host) throws IOException {
		this(host, DEFAULT_PORT);
	}

	public FtpUtil(String host, int port) throws IOException {
		this.host = host;
		this.port = port;
	}

	protected void connect() throws IOException {
		connection = new Socket(host, port);
		_reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), encode));
		_writer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream(), encode));
		readResponse();
	}

	public void login(String user, String passwd) throws IOException {
		connect();
		
		sendComand("USER " + user);
		sendComand("PASS " + passwd);
	}

	public String pwd() throws IOException {
		FtpResponse resp = sendComand("PWD");
		if (!resp.success()) return null;

		String info = resp.getLastInfo();
		Pattern pattern = Pattern.compile("\\\"(.*)?\\\"");
		Matcher matcher = pattern.matcher(info);
		if (matcher.find()) return matcher.group(1);
		return null;
	}

	public boolean cwd(String dir) throws IOException {
		FtpResponse resp = sendComand("CWD " + dir);
		return resp.success();
	}

	public List<FtpFile> listFile() throws IOException {
		return listFile(null);
	}

	public List<FtpFile> listFile(String remotePath) throws IOException {
		Socket pasvSocket = pasv();
		sendComand("LIST" + (showHideFile ? " -a" : "") + (remotePath == null ? "" : (" " + remotePath)));
		String content = IOUtils.getSocketContent(pasvSocket, encode);
		readResponse();

		List<FtpFile> files = new ArrayList<>();
		if (!"".equals(content)) {
			String[] lines = content.split("[\\r\\n]");;
			for (String line : lines) {
				if (line.trim().isEmpty()) continue;
				files.add(FtpFile.parser(line));
			}
		}
		return files;
	}

	public FtpFile file(String remoteFile) throws IOException {
		Socket pasvSocket = pasv();
		sendComand("LIST " + remoteFile);
		String content = IOUtils.getSocketContent(pasvSocket, encode);
		readResponse();
		return FtpFile.parser(content);
	}

	public void downFile(String remoteFile, String savePath) throws IOException {
		downFile(remoteFile, savePath, null);
	}

	public void downFile(String remoteFile, String savePath, Consumer<Long> listen) throws IOException {
		File file = new File(savePath);
		if (!file.getParentFile().exists()) file.getParentFile().mkdirs();

		downStream(remoteFile, in -> {
			FileOutputStream out = new FileOutputStream(file);
			IOUtils.copyStream(in, out, listen);
			out.close();
		});
	}

	public void downStream(String remoteFile, StreamHandler<InputStream> handler) throws IOException {
		Socket pasvSocket = pasv();
		sendComand("RETR " + remoteFile);
		InputStream in = pasvSocket.getInputStream();
		handler.handle(in);
		readResponse();
	}

	public void uploadFile(String remoteFile, String localFile) throws IOException {
		FileInputStream in = new FileInputStream(localFile);
		uploadStream(remoteFile, in);
		in.close();
	}

	public void uploadStream(String remoteFile, InputStream in) throws IOException {
		uploadStream(remoteFile, in, null);
	}

	public void uploadStream(String remoteFile, InputStream in, Consumer<Long> listen) throws IOException {
		Socket pasvSocket = pasv();
		sendComand("STOR " + remoteFile);
		OutputStream out = pasvSocket.getOutputStream();
		IOUtils.copyStream(in, out, listen);
		out.flush();
		out.close();
		readResponse();
	}

	public void deleteFile(String remotePath) throws IOException {
		sendComand("DELE " + remotePath);
	}

	public void deleteDirectory(String remoteDirectory, boolean recursion) throws IOException {
		if (recursion) {
			List<FtpFile> listFile = listFile(remoteDirectory);
			for (FtpFile ftpFile : listFile) {
				String path = remoteDirectory + "/" + ftpFile.getName();
				if (ftpFile.isDirectory()) deleteDirectory(path, true);
				else deleteFile(path);
			}
		}
		sendComand("RMD " + remoteDirectory);
	}

	public Socket pasv() throws IOException {
		FtpResponse resp = sendComand("PASV");
		if (!resp.success()) return null;
		Object[] addInfo = pasvAddrssParser(resp.getLastInfo());
		Socket socket = new Socket((String) addInfo[0], (int) addInfo[1]);
		return socket;
	}

	public void write(String cmd) throws IOException {
		_writer.write(cmd);
		_writer.newLine();
		_writer.flush();
		if (logListener != null) {
			logListener.log(1, cmd);
		}
	}

	public FtpResponse readResponse() throws IOException {
		FtpResponse resp = new FtpResponse();
		for (;;) {
			String line = _reader.readLine();
			if (line == null) break;
			if (logListener != null) {
				logListener.log(2, line);
			}
			
			if (line.matches("^\\d{3} .*$")) {
				resp.setCode(Integer.valueOf(line.substring(0, 3)));
				resp.putInfo(line.substring(4));
				break;
			} else resp.putInfo(line);
		}
		return resp;
	}

	public FtpResponse sendComand(String cmd) throws IOException {
		write(cmd);
		return readResponse();
	}

	public void disconnect() throws IOException {
		sendComand("QUIT");
		connection.close();
	}

	public String getHost() {
		return host;
	}

	public void setHost(String host) {
		this.host = host;
	}

	public int getPort() {
		return port;
	}

	public void setPort(int port) {
		this.port = port;
	}

	public boolean isShowHideFile() {
		return showHideFile;
	}

	public void setShowHideFile(boolean showHideFile) {
		this.showHideFile = showHideFile;
	}

	public String getEncode() {
		return encode;
	}

	public void setEncode(String encode) {
		this.encode = encode;
	}

	public LogListener getLogListener() {
		return logListener;
	}

	public void setLogListener(LogListener logListener) {
		this.logListener = logListener;
	}

	private static Object[] pasvAddrssParser(String info) {
		Pattern pattern = Pattern.compile("\\((.*)?\\)");
		Matcher matcher = pattern.matcher(info);
		if (!matcher.find()) return null;
		String _str = matcher.group(1);
		int index1, index2, p1, p2;
		index1 = _str.lastIndexOf(",");
		p1 = Integer.valueOf(_str.substring(index1 + 1).trim());
		index2 = _str.lastIndexOf(",", index1 - 1);
		p2 = Integer.valueOf(_str.substring(index2 + 1, index1).trim());
		String host = _str.substring(0, index2).replaceAll(",", ".");
		int port = p2 * 256 + p1;
		return new Object[] { host, port };
	}
}
